Skip to content

Instantly share code, notes, and snippets.

@zmajstor
Last active December 12, 2023 11:39
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save zmajstor/418b8b392f124bfc5d4e0cbf35a0d8cb to your computer and use it in GitHub Desktop.
Save zmajstor/418b8b392f124bfc5d4e0cbf35a0d8cb to your computer and use it in GitHub Desktop.
Docker on CentOS Linux

NGINX reverse-proxy with Let's Encrypt Certs with Docker Compose

Acts as a single reverse proxy with SSL offloading for any number of dockerized web projects on a single server, using 2 awesome projects:

jwilder/nginx-proxy

  • the reverse proxy is be the only container that actually needs to expose any ports to the world.
  • base image is just a regular nginx
  • the only container that actually exposes anything to the outside world, both port 80 and 443
  • any other Docker Web Container or Docker-Compose stack with a specific environment variables, that lands in the same bridge network, will automatically be reverse-proxied and provided with a SSL certificate from LetsEncrypt.
  • has read-only access to the docker host's docker.sock
    • this is how the docker host will notify this container about other containers starting or stopping
  • will watch for any new container with an environment variable called "VIRTUAL_HOST"
    • if any such starting container is found, it will update it's nginx.tmpl file, create a new server directive for this new container and push the changes to the proxy container
    • similarly, if any such container is stopped, it will remove the corresponding entry from the server directives and push the updated config to the proxy container
  • gains read-only access to ./data/proxy/certs

letsencrypt

  • will handle obtaining and renewing certificates from letsEncrypt
  • also has read-only access to the docker host's docker.sock
    • just like the docker-gen container, this container will be notified about any starting or stopping containers
  • will watch for any new container with environment properties called "LETSENCRYPT_HOST" and "LETSENCRYPT_EMAIL"
    • will check if there are certificate files for the domain name set in the "LETSENCRYPT_HOST" property of any such started container
      • if none are found, it will try to obtain new certificate files for this domain, also using the "LETSENCRYPT_EMAIL" address
      • if certificate files for this domain already exists, it will try to renew any certificate that would expire within 30 days every 3600 seconds

Swarm job scheduler (cron implementation through go routines)

https://github.com/crazy-max/swarm-cronjob

Cron entries are stored in an array, sorted by their next activation time. Cron sleeps until the next job is due to be run. Upon waking:

  • it runs each entry that is active on that second
  • it calculates the next run times for the jobs that were run
  • it re-sorts the array of entries by next activation time.
  • it goes to sleep until the soonest job.

https://godoc.org/github.com/crazy-max/cron#hdr-Predefined_schedules

Entry                  | Description                                | Equivalent To
-----                  | -----------                                | -------------
@yearly (or @annually) | Run once a year, midnight, Jan. 1st        | 0 0 0 1 1 *
@monthly               | Run once a month, midnight, first of month | 0 0 0 1 * *
@weekly                | Run once a week, midnight between Sat/Sun  | 0 0 0 * * 0
@daily (or @midnight)  | Run once a day, midnight                   | 0 0 0 * * *
@hourly                | Run once an hour, beginning of hour        | 0 0 * * * *
@every <duration>      | Schedule after <duration></duration>

For example, @every 1h30m10s would indicate a schedule that activates after 1 hour, 30 minutes, 10 seconds, and then every interval after that. Note: The interval does not take the job runtime into account. For example, if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, it will have only 2 minutes of idle time between each run.

#!/bin/sh
set -e
# source: https://docs.docker.com/install/linux/docker-ce/centos/
echo "cleanup old stuff ..."
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
echo "installing docker-ce & cli"
sudo yum install -y docker-ce docker-ce-cli containerd.io
sudo systemctl start docker
echo "docker install finished"
docker --version
## installing docker-compose
#### check for the latest release on https://github.com/docker/compose/releases
sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
#!/bin/sh
set -e
yum install -y yum-utils device-mapper-persistent-data lvm2
yum install -y docker-ce
sudo systemctl enable docker.service
sudo systemctl daemon-reload && sudo systemctl restart docker.service

Configure dockerd to start on boot (systemd)

Enable docker

sudo systemctl enable docker

Edit docker service

sudo systemctl edit docker.service

... and enter:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://127.0.0.1:2376

... and after that restart service:

sudo systemctl daemon-reload && sudo systemctl restart docker.service

Add docker-compose service

sudo vi /etc/systemd/system/docker-compose-app.service
[Unit]
Description=Docker infra
Requires=docker.service
After=docker.service

[Service]
Restart=always
ExecStart=/usr/local/bin/docker-compose -f /root/docker-compose.yml up -d
ExecStop=/usr/local/bin/docker-compose -f /root/docker-compose.yml down

[Install]
WantedBy=default.target
systemctl enable docker-compose-app
systemctl daemon-reload && systemctl restart docker.service

Useful commands to check running service

sudo netstat -lntp | grep dockerd
sudo netstat -tuplen
curl http://127.0.0.1:2376/images/json
# docker network create --driver overlay --attachable webproxy_network
# docker-compose up -d
version: '2'
services:
nginx-proxy:
image: jwilder/nginx-proxy
container_name: nginx-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- dhparam:/etc/nginx/dhparam
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
networks:
- webproxy_network
letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: nginx-proxy-le
restart: always
volumes_from:
- nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- webproxy_network
portainer:
image: portainer/portainer
container_name: portainer-web
command: -H unix:///var/run/docker.sock
restart: always
ports:
- 9000:9000
environment:
- "VIRTUAL_PORT=9000"
- "VIRTUAL_HOST=portainer.example.com"
- "LETSENCRYPT_HOST=portainer.example.com"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
- webproxy_network
- portainer_agent_network
networks:
webproxy_network:
external: true
portainer_agent_network:
external: true
volumes:
portainer_data:
conf:
vhost:
html:
dhparam:
certs:
#!/bin/sh
set -e
## Building Docker Image on Local Workstation
REPO_USER=myuser
APPNAME=myapp
REPOIMAGE="${REPO_USER}/${APPNAME}"
docker login -u $REPO_USER
docker build -t $APPNAME . && \
docker tag $APPNAME:latest $REPOIMAGE:latest && \
docker push $REPOIMAGE:latest
#!/bin/sh
docker service create \
--name cron_scheduler \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
-e "TZ=Europe/Paris" \
-e "LOG_LEVEL=debug" \
-e "LOG_JSON=false" \
--constraint "node.role == manager" \
crazymax/swarm-cronjob
FROM ruby:2.5.3-alpine
RUN apk --no-cache --update add git nodejs postgresql-client postgresql-dev tzdata yarn
# && npm install -g -s --no-progress yarn
# python3 && pip3 --no-cache-dir install awscli s3cmd
# throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1
WORKDIR /app
# Different layer for gems installation
COPY Gemfile Gemfile.lock package.json ./
RUN apk --no-cache add --virtual build-deps build-base ruby-dev && \
bundle install --without development test --deployment --no-cache --jobs `expr $(cat /proc/cpuinfo | grep -c "cpu cores") - 1` --retry 3 && \
apk del build-deps
# Copy the application into the container
COPY . .
# precompile assets with dummy SECRET_KEY_BASE
RUN SECRET_KEY_BASE=1 RAILS_ENV=production DISABLE_SPRING=1 bin/rails assets:precompile
# cleanup
RUN find / -type f -iname \*.apk-new -delete && \
rm -rf /var/cache/apk/* && \
rm -rf /usr/lib/lib/ruby/gems/*/cache/* && \
rm -rf ~/.gem && \
rm -rf /var/lib/apt/lists/* && \
rm -rf /tmp/* && \
yarn cache clean
# RUN chown -R nobody:nogroup /app
# USER nobody
# ENV RACK_ENV=production \
# RAILS_ENV=production \
# RAILS_LOG_TO_STDOUT=1 \
# RAILS_SERVE_STATIC_FILES=1 \
# PORT=3000
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment