Docker Build/Deploy using Bash Script - bash

I have a deploy script that I am trying to use for my server for CD but I am running into issues writing the bash script to complete some of my required steps such as running npm and the migration commands.
How would I go about getting into a container bash, from this script, running the commands below and then exiting to finish pulling up the changes?
Here is the script I am trying to automate:
cd /Project
docker-compose -f docker-compose.prod.yml down
git pull
docker-compose -f docker-compose.prod.yml build
# all good until here because it opens bash and does not allow more commands to run
docker-compose -f docker-compose.prod.yml run --rm web bash
npm install # should be run inside of web bash
python manage.py migrate_all # should be run inside of web bash
exit # should be run inside of web bash
# back out of web bash
docker-compose -f docker-compose.prod.yml up -d

Typically a Docker image is self-contained, and knows how to start itself up without any user intervention. With some limited exceptions, you shouldn't ever need to docker-compose run interactive shells to do post-deploy setup, and docker exec should be reserved for emergency debugging.
You're doing two things in this script.
The first is to install Node packages. These should be encapsulated in your image; your Dockerfile will almost always look something like
FROM node
WORKDIR /app
COPY package*.json .
RUN npm ci # <--- this line
COPY . .
CMD ["node", "index.js"]
Since the dependencies are in your image, you don't need to re-install them when the image starts up. Conversely, if you change your package.json file, re-running docker-compose build will re-run the npm install step and you'll get a clean package tree.
(There's a somewhat common setup that puts the node_modules directory into an anonymous volume, and overwrites the image's code with a bind mount. If you update your image, it will get the old node_modules directory from the anonymous volume and ignore the image updates. Delete these volumes: and use the code that's built into the image.)
Database migrations are a little trickier since you can't run them during the image build phase. There are two good approaches to this. One is to always have the container run migrations on startup. You can use an entrypoint script like:
#!/bin/sh
python manage.py migrate_all
exec "$#"
Make this script be executable and make it be the image's ENTRYPOINT, leaving the CMD be the command to actually start the application. On every container startup it will run migrations and then run the main container command, whatever it may be.
This approach doesn't necessarily work well if you have multiple replicas of the container (especially in a cluster environment like Docker Swarm or Kubernetes) or if you ever need to downgrade. In these cases it might make more sense to manually run migrations by hand. You can do that separately from the main container lifecycle with
docker-compose run web \
python manage.py migrate_all
Finally, in terms of the lifecycle you describe, Docker images are immutable: this means that it's safe to rebuild new images while the old ones are running. A minimum-downtime approach to the upgrade sequence you describe might look like:
git pull
# Build new images (includes `npm install`)
docker-compose build
# Run migrations (if required)
docker-compose run web python manage.py migrate_all
# Restart all containers
docker-compose up --force-recreate

Related

How to prepare the shell environment in an image executed by GitLab Runner?

I'm running CI jobs on a self-hosted GitLab instance plus 10 GitLab Runners.
For this minimal example, two Runners are needed:
Admin-01
A shell runner with Docker installed.
It can execute e.g. docker build ... to create new images, which are then pushed to the private Docker registry (also self-hosted / part of the GitLab installation)
Docker-01
A docker runner, which executes the previously build image.
On a normal bare-metal, virtual machine or shell runner, I would modify e.g. ~/.profile to execute commands before before_script or script sections are executed. In my use case I need to set new environment variables and source some configuration files provided by the tools I want to run in an image. Yes, environment variables could be set differently, but there seams to be no way to source Bash scripts automatically before before_script or script sections are executed.
When sourcing the Bash source file manually, it works. I also notice, that I have to source it again in script block. So I assume the Bash session is ended between before_script block to script block. Of cause, it's no nice solution to manually source the tools Bash configuration script in every .gitlab-ci.yml file manually by the image users.
myjobn:
# ...
before_script:
- source /root/profile.additions
- echo "PATH=${PATH}"
# ...
script:
- source /root/profile.additions
- echo "PATH=${PATH}"
# ...
The mentioned modifications for e.g. shell runners does not work in images executed by GitLab Runner. It feels like the Bash in the container is not started as login shell.
The minimal example image is build as follows:
fetch debian:bullseye-slim from Docker Hub
use RUN commands in Dockerfile to modify with some echo outputs
/etc/profile
/root/.bashrc
/root/.profile
# ...
RUN echo "echo GREETINGS FROM /ROOT/PROFILE" >> /root/.profile \
&& echo "echo GREETINGS FROM /ETC/PROFILE" >> /etc/profile \
&& echo "echo GREETINGS FROM /ROOT/BASH_RC" >> /root/.bashrc
When the job starts, non of the echos is printing messages, while a cat shows, the echo commands have been put at the right places while building the image.
At next I tried to modify
SHELL ["/bin/bash", "-l", "-c"]
But I assume, this has only effects in RUN commands in the Dockerfile, but not on an executed container.
CMD ["/bin/bash", "-l"]
I see no behavior changes.
Question:
How to start the Bash in the Docker image managed by GitLab Runner as login shell so it ready configuration scripts?
How to modify the environment in a container before before_script or script runs. Modifying means environment variables and execution / sourcing a configuration script or patched default script like ~/.profile.
How does GitLab Runner execute a job with Docker?
This is not documented by GitLab in the official documentation ...
What I know so far, it jumps between Docker images specified by GitLab and user defined images and shares some directories/volumes or so.
Note:
Yes, the behavior can be achieved with some Docker arguments in docker run, but as I wrote GitLab Runner is managing the container. Alternatively, how to configure, how GitLab Runner launches the images? To my knowledge, there is no configuration option available / documented for this situation.
A shell runner with Docker installed. It can execute e.g. docker build ...
Use docker-in-docker or use kaniko. https://docs.gitlab.com/ee/ci/docker/using_docker_build.html
Shell executor is like "the last resort", where you want specifically to make changes to the server, or you are deploying your application "into" this server.
How to start the Bash in the Docker image managed by GitLab Runner as login shell so it ready configuration scripts?
Add ENTRYPOING bash -l to your image. Or set the entrypoint from gitlab-ci.yml. See docker documentation on ENTRYPOINT and gitlab-ci.yml documentation on image: entrypoint: .
How to modify the environment in a container before before_script or script runs.
Build the image with modified environment. Consult Dockerfile documentation on ENV statements.
Or set the environment from gitlab-ci.yml file. Read documentation on variables: in gitlab-ci.
How to prepare the shell environment in an image executed by GitLab Runner?
Don't. The idea is that the environment is reproducible, ergo, there should be no changes beforehand. Add variables: in gitlab-ci file and use base images if possible.
How does GitLab Runner execute a job with Docker?
This is not documented by GitLab in the official documentation ...
Gitlab is open-source.
What I know so far, it jumps between Docker images specified by GitLab and user defined images and shares some directories/volumes or so.
Yes, first a gitlab-runner-helper is executed - it has git and git-lfs and basically clones the repository and downloads and uploads the artifacts. Then the container specified with image: is run, cloned repository is copied into it and a specially prepared shell script is executed in it.

Copy contents from host OS into Docker image without rebuilding image

I'm building a new image and copy contents from host OS folder D:\Programs\scrapy into it like so: docker build . -t scrapy
Dockerfile
FROM mcr.microsoft.com/windows/servercore:ltsc2019
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
RUN mkdir root
RUN cd root
WORKDIR /root
RUN mkdir scrapy
COPY scrapy to /root/scrapy
Now when I add new contents to the host OS folder "D:\Programs\scrapy" I want to also add it to image folder "root/scrapy", but I DON'T want to build a completely new image (it takes quite a while).
So how can I keep the existing image and just overwrite the contents of the image folder "root/scrapy".
Also: I don't want to copy the new contents EACH time I run the container (so NOT at run-time), I just want to have a SEPARATE command to add more files to an existing image and then run a new container based on that image at another time.
I checked here: How to update source code without rebuilding image (but not sure if OP tries to do the same as me)
UPDATE 1
Checking What is the purpose of VOLUME in Dockerfile and docker --volume format for Windows
I tried the commands below, all resulting in error:
docker: Error response from daemon: invalid volume specification: ''. See 'docker run --help'.
Where <pathiused> is for example D:/Programs/scrapy:/root/scrapy
docker run -v //D/Programs/scrapy:/root/scrapy scrapy
docker run -v scrapy:/root/scrapy scrapy
docker run -it -v //D/Programs/scrapy:/root/scrapy scrapy
docker run -it -v scrapy:/root/scrapy scrapy
UPDATE WITH cp command based on #Makariy's feedback
docker images -a gives:
REPOSITORY TAG IMAGE ID CREATED SIZE
scrapy latest e35e03c8cbbd 29 hours ago 5.71GB
<none> <none> 2089ad178feb 29 hours ago 5.71GB
<none> <none> 6162a0bec2fc 29 hours ago 5.7GB
<none> <none> 116a0c593544 29 hours ago 5.7GB
mcr.microsoft.com/windows/servercore ltsc2019 d1724c2d9a84 5 weeks ago 5.7GB
I run docker run -it scrapy and then docker container ls which gives:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1fcda458a14c scrapy "c:\\windows\\system32…" About a minute ago Up About a minute thirsty_bassi
If I run docker cp D:\Programs\scrapy scrapy:/root/scrapy I get:
Error: No such container:path: scrapy:\root
So in a separate PowerShell instance I then run docker cp D:\Programs\scrapy thirsty_bassi:/root/scrapy whichs show no output in PowerShell whatsoever, so I think it should've done something.
But then in my container instance when I goto /root/scrapy folder I only see the files that were already added when the image was built, not the new ones I wanted to add.
Also, I think I'm adding files to the container here, but is there no way to add it to the image instead? Without rebuilding the whole image?
UPDATE 2
My folder structure:
D:\Programs
Dockerfile
\image_addons
Dockerfile
\scrapy
PS D:\Programs>docker build . -t scrapybase
Successfully built 95676d084e28
Successfully tagged scrapybase:latest
PS D:\Programs\image_addons> docker build -t scrapy .
Step 2/2 : COPY scrapy to /root/scrapy
COPY failed: file not found in build context or excluded by .dockerignore: stat to: file does not exist
Dockerfile A
FROM mcr.microsoft.com/windows/servercore:ltsc2019
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
WORKDIR /root/scrapy
Dockerfile B
FROM scrapybase
COPY scrapy to /root/scrapy
You also can use docker cp, to manually copy files from your host to running container
docker cp ./path/to/file containername:/another/path
Docs
answer if you want it quick and dirty
docker run -it -v c:/programs/test:/root/test ubuntu:latest cat /root/test/myTestFile.txt
to update one file quickly:
If you don't have to build your code (I don't know what language you are using) you can build some base image with the initial code and when you want to change only one file (again I'm assuming you don't need to compile your project again for that, otherwise if you do that is not possible to due the nature of compiled programming language):
FROM previous-version-image:latest
COPY myfile dest/to/file
then because your CMD and ENTRYPOINT are saved from the previous stages no need to declare them. (if you don't remember use docker history <docker-image-name> to view virtual dockerfile for image to this stage).
Notice though not to repetitively use this method or you'll get a very big image with many useless layers. Use this only for quick testing and debugging.
explanation
Usually people use it for frontend development on docker containers but the basic idea persists, you create the basic working image with the dependencies installed and the directory layout setup with the last Dockerfile command being the development server start command.
example:
Dockerfile:
# pull the base image
FROM node:slim
# set the working directory
WORKDIR /app
# add `/app/node_modules/.bin` to $PATH
ENV PATH /app/node_modules/.bin:$PATH
# copy dependencies files
COPY package.json ./
COPY package-lock.json ./
# install app dependencies
RUN npm install
# add app
COPY . ./
# start development server
CMD ["npm", "start"]
startup command:
docker run -it --rm \
-v ${PWD}:/app \ <mount current working directory in host to container in path /app>
-v /app/node_modules \ <or other dependency directory if exists>
-p 80:3000 \ <ports if needs exposing>
ps-container:dev
I'm not sure if that use case will 100% work for you because it needs the code to be mounted using bind-mount all the time and when needed to be exported will have to be exported as the image and the source code directory, on the other hand, it allows you to make quick changes without waiting for the image to be built each time you add something new and in the end build the final image that contains all that's needed.
more relatable example to question provided code:
As you can see there is a file on the host machine that contains some text
the command that uses bind-mount to have access to the file:
docker run -it -v c:/programs/test:/root/test ubuntu:latest cat /root/test/myTestFile.txt
hope you find something that works for you from what I've provided here.
thanks to this tutorial and this example for starting examples and information.
EDIT:
Let's say your original Dockerfile looks like this:
FROM python:latest
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD python /app/app.py
This will build your initial image on top of we'll add layers and change the python files.
The next Dockerfile we'd use (let's call it Dockerfile.fix file) would copy the file we want to change instead of the ones already in the image
FROM previous-image-name
COPY app.py .
Now with after building with this Dockerfile the final image Dockerfile would look (sort of) like so:
FROM python:latest
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD python /app/app.py
FROM previous-image-name
COPY app.py .
And each time we'll want to change the file we'll use the second Dockerfile
There's no way you can change a Docker image without (at least partially) rebuilding it. But you don't have to rebuild all of it, you can just rebuild the layer copying your scrapy content.
You can optimize your build to have two images:
First image is your static image you don't want to rebuild each time. Let's call it scrapy-base.
Second and final image is based on first image scrapy-base and will only exist for the purpose of copying your dynamic scrapy content
scrapy-base's Dockerfile:
FROM mcr.microsoft.com/windows/servercore:ltsc2019
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
RUN mkdir root
RUN cd root
WORKDIR /root
RUN mkdir scrapy
And build it like:
docker build -t scrapy-base .
This command only needs to be run once. You won't have to build this image if you only change the content of local scrapy folder. (as you can see, the build does not use it at all)
scrapy's Dockerfile:
FROM scrapy-base
COPY scrapy /root/scrapy
With build command:
docker build -t scrapy .
This second build command will re-use the previous static image and only copy content without having to rebuild the entire image. Even with lots of files it should be pretty quick. You don't need to have a running container.
For your scenario :
docker run -v D:/test:/root/test your-image
A lots of valuable details available in this thread

Heroku with container stack and ENTRYPOINT instruction continues to run with "/bin/sh -c"

I'll first preface with relevant sections in Heroku's documentation on their container registry and runtime for Docker deploys.
The Docker commands and runtime section states:
ENTRYPOINT is optional. If not set, /bin/sh -c will be used
CMD will always be executed by a shell so that config vars are made
available to your process; to execute single binaries or use images
without a shell please use ENTRYPOINT
I want to highlight that last sentence (emphasis added):
to execute single binaries or use images without a shell please use ENTRYPOINT
Furthermore, in the Unsupported Dockerfile commands section.
SHELL - The default shell for Docker images is /bin/sh, you can override
with ENTRYPOINT if necessary.
My understanding from this is that if the ENTRYPOINT instruction exists in my Dockerfile, then Heroku's container stack will not execute the command with /bin/sh. But apparently my understanding is wrong, because that seems to be still happening.
Here is an illustration of what my Dockerfile looks like:
FROM golang:1.14 as build
# ... builds the binary
FROM scratch
# ... other irrelevant stuff
COPY --from=build /myprogram /myprogram
ENTRYPOINT ["/myprogram"]
The final image contains the binary at /myprogram in a scratch base image. Since scratch doesn't have /bin/sh, it is necessary to override this. According to their documentation, this is done with ENTRYPOINT.
However, when I deploy it to Heroku, it appears to still execute with a shell. To illustrate:
% heroku ps -a my-app-name
== web (Free): /bin/sh -c --myflag1 --myflag2 (1)
Which means it's ultimately executing:
/myprogram /bin/sh -c --myflag1 --myflag2
Which is obviously not what I want. But what happened to this part of the documentation (which I highlighted earlier)?:
to execute single binaries or use images without a shell please use ENTRYPOINT
...
The heroku.yml file looks like the following:
---
build:
docker:
web: Dockerfile
run:
web:
command:
- --myflag1
- --myflag2
I still have the same problem with the shell form and the exec form. Meaning, I also tried with the heroku.yml file looking like this:
---
build:
docker:
web: Dockerfile
run:
web: --myflag1 --myflag2
Now, I know that I can get everything to "just work" by basing the final image on an image that has /bin/sh and remove the ENTRYPOINT instruction, specifying the command with /myprogram. I don't need to use scratch, but I want to, and should be able to use scratch, right? It's what I've been using for many years to run my statically linked binaries in containers and I have never come across problems like this when deploying on other platforms.
So am I misunderstanding their documentation? What do I need to do to get rid of this /bin/sh shenanigan?

How to run node.js with gulp inside docker and find the right bashsrc

I am new to Docker and I do have some problems.
My goal:
use the Dockerfile to create a docker-container and stay inside the container / don't drop out of it.
running a local Docker-Container
installing "gulp" with the package.json
installing "gulp global" on Docker
copy any files to my Docker container
execute "gulp --version" and the default "gulp task" and stay inside the terminal.
Here is my setup:
Dockerfile
FROM node:10
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --global gulp-cli
RUN npm install
COPY . .
EXPOSE 3000
CMD ["gulp --version" , "gulp"] or? [gulp --version , gulp]
package.json
{
"name": "docker-test",
"version": "1.0.0",
"description": "Testing Docker",
"main": "index.js",
"scripts": {
"test": "test"
},
"author": "",
"license": "",
"devDependencies": {
"gulp": "^4.0.0"
}
}
gulpfile.js
function defaultTask(cb) {
console.log("Default task for gulp 4.0 for docker")
cb();
}
exports.default = defaultTask
docker-compose.yml (I don't think we need this for my question but I will post it anyway since I am not exactly sure If this could make some problems)
version: '3'
services:
html:
container_name: gulp-docker-test
restart: always
build: .
ports:
- '80:3000'
My problems right now:
First of all I am really confused about the workflow of docker.
Do I understand it correctly if I run:
docker build . --tag gulp-docker-test
I will create a new docker-container on my computer with the content of the dockerfile?
If I need to update anything inside it I have to run it again so the container is updated?
If I use:
docker start gulp-docker-test
it will start the container? What if I change anything inside it? Will it be back on reboot of the container? Or is it gone because it is only a temporary image?
Beside that if I try to run it I get this error:
ERROR: for gulp-docker-test Cannot start service html: OCI runtime create failed: container_linux.go:344: starting container process caused "exec: \"gulp --version\": executable file not found in $PATH": unknown
ERROR: for html Cannot start service html: OCI runtime create failed: container_linux.go:344: starting container process caused "exec: \"gulp --version\": executable file not found in $PATH": unknown
I did try those things: execute it with exec, removing the quotes inside the CMD of the Dockerfile but I think I do have some basic knowledge missing. I don't understand how to boot this container inside the shell so docker knows the $path.
Thank you for your help in advance
Edit:
I did found out how to run docker with the shell.
docker run -it --entrypoint bash gulp-docker-test3
root#8a27dc3a9c85:/usr/src/app# gulp -v
[15:01:53] CLI version 2.0.1
[15:01:53] Local version 4.0.0
root#8a27dc3a9c85:/usr/src/app# gulp
[15:02:38] Using gulpfile /usr/src/app/gulpfile.js
[15:02:38] Starting 'default'...
Default Task von Gulp 4.0 für Docker
[15:02:38] Finished 'default' after 4.28 ms
root#8a27dc3a9c85:/usr/src/app#
It looks like it should work if I can add default bash to the dockerfile.
If I run docker build I will create a new container?
It will execute the contents of the Dockerfile, and create a new image.
You need to docker run (or, rarely, docker create) the image to create a container from it. When you update the Dockerfile or your application source, you need to repeat the docker build step, docker stop && docker rm the existing container, and docker run a new one. Your docker-compose.yml fragment encapsulates this, but note that Docker Compose will delete and recreate a container when it's appropriate.
If I use docker start gulp-docker-test...
It will start a container with that name. That's a separate namespace from the image namespace. The container has to already exist and be stopped (usually from an explicit docker stop command). This is a slightly unusual state to be in.
CMD ["gulp --version" , "gulp"]
This looks for a binary named gulp --version, and runs it with a single parameter gulp. Since you probably don't have a single file named /usr/local/bin/gulp --version (with the spaces and "version" as part of the filename) you get an error.
You only get one CMD in a Dockerfile. (Or one ENTRYPOINT instead, but I tend to find CMD preferable except in a couple of extremely specific cases.) Each "word" you'd type in a shell becomes a separate "word" in the syntax. So you could, for instance, write
CMD ["gulp", "--version"]
Alternatively, if you leave off the brackets, Docker will wrap the CMD text in sh -c ..., so something closer to what you actually wrote is
CMD gulp --version && gulp
In practice you'd usually run build tools like Gulp as part of building the image, and use the CMD to actually start your application.
RUN gulp
CMD ["npm", "start"]
First
I will create a new docker-container on my computer with the content
of the dockerfile? If I need to update anything inside it I have to
run it again so the container is updated?
docker-build creates an image (like ISO). To create a container, you have to start/run this image. Container is a running image, which can differ from original because during run you can modify the file system inside. When you stop and remove the container, all changes are lost. Docker practice is not to store data in images - if image produces something valuable, it should be stored outside (consider volumes for that).
Second
CMD ["gulp --version" , "gulp"]
This is incorrect. JSON notation requires you to put each argument in a separate array element. This is correct:
CMD ["guld", "--version"]
Conclusion
You create an image with
docker build -t my-image .
You start it (create container) with
docker run --name=my-image-instance my-image
If you need to control running container, you can use friendly name my-image-instance or, if you didnt provide it, container's ID

AWS docker set --no-cache flag

I am using EB on AWS to deploy a dockerfile.
Currently I deploy to scripts:
The dockerfile and a run.sh file which starts a server.
The dockerfile roughly looks like this
FROM ubuntu:14.04
MAINTAINER xy
[...install a java server...]
ADD run.sh /run.sh
RUN chmod +x /*.sh
EXPOSE 8080
CMD ["/run.sh"]
run.sh starts the java server.
I would like to set the --no-cache flag for the docker. Where can I set that?
You can't specify docker build's --no-cache because eb doesn't allow you to.
A workaround is to build the image locally (using --no-cache). Then use docker push to publish your image to Docker hub public registry.
Your Dockerfile could be simplified (untested) down to:
FROM custom_java_server_build:latest
MAINTAINER xy
EXPOSE 8080
CMD ["/run.sh"]
It does sound like you're creating a large image, you might be able to mitigate this by turning the entire install sequence into a single RUN statement. Don't forget to delete all your temporary files too.
You use --no-cache only in the docker build step. If the run script doesn't build the image, then you need to find what is building it and tell that process to use no-cache.

Resources