Build a Basic Web Service on Docker Compose
Docker Compose is Docker but with some extended functionality. Its a great way to script complicated deployments in a human readable format. In this post Docker Compose is installed on a server and a basic Nginx page is setup to prove functionality. An additional package is added to show extendibility.
This is a post is written with the beginner in mind.
0 . Latest updates and prerequisites.
2023.09.25 – First draft, Ubuntu 22.04 Server.
Check back for updates, if you run into trouble. Leave a comment if the post is missing some detail.
You need sudo or root access to install and configure packages. Previous posts, covering certificates, notifications and static IP address setup are recommended. If you intend the server to be publicly reachable, look at DDNS with DigitalOcean.
1 . Install Docker.
First step, install docker-compose. This package includes Docker as well as all the dependencies to allow for a much more complicated deployment later.
sudo apt install docker-compose-v2 -yYou can test the deployment with a simple hello world image, made for testing:
docker run hello-worldThe output of this command should look like this:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
719385e32844: Pull complete
Digest: sha256:4f53e2564790c8e7856ec08e384732aa38dc43c52f02952483e3f003afbf23db
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
Now, create a more complicated deployment, but in a configuration file.
2 . Create a Docker configuration file.
Docker-compose allows for a configuration file to handle many of the settings required to maintain a more complicated environment. This file will help to keep everything in order. The file should be written to be human readable:
sudo mkdir -p /opt/docker
sudo nano /opt/docker/docker-compose.ymlThe file is extremely strict on format, be sure to manage indentation; make a basic file, like this:
services:
nginx:
container_name: nginx
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- "/opt/docker/nginx/config:/etc/nginx/conf.d:ro"
- "/opt/docker/nginx/logs:/var/log/nginx"
- "/opt/docker/nginx/html:/usr/share/nginx/html"
- "/opt/sslupdate/certificates:/certificates"
restart: unless-stopped
In the ports and volumes sections, detail is divided by a colon, the first part is what exists locally on the server and the second part is what exists from the container’s perspective. Of course the local resources must exist and be free or Docker will not load the container. For example, ports 80 and 443 should be free on the local server and the directories within /opt should also exist. Finally, the conf.d folder, in the container, will be read-only, as noted by ‘:ro’ in the configuration.
Save the file and run it, from within the /opt/docker directory:
root@server40:/# cd /opt/docker/
root@server40:/opt/docker# docker compose up -d
Creating network "docker_default" with the default driver
Pulling nginx (nginx:latest)...
latest: Pulling from library/nginx
a803e7c4b030: Pull complete
8b625c47d697: Pull complete
4d3239651a63: Pull complete
0f816efa513d: Pull complete
01d159b8db2f: Pull complete
5fb9a81470f3: Pull complete
9b1e1e7164db: Pull complete
Digest: sha256:32da30332506740a2f7c34d5dc70467b7f14ec67d912703568daff790ab3f755
Status: Downloaded newer image for nginx:latest
Creating nginx ... done
Docker downloads the image tagged as ‘latest’ since that is what the configuration file specifies. The “up -d” means to load up the configuration in daemon mode (as a service). Nginx is now running, as is evident with “docker ps”:
root@server40:/opt/docker# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
431d8c2e491a nginx:latest "/docker-entrypoint.…" 35 seconds ago Up 34 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp nginx
Docker “ps” option shows what resources are being used. You can see the ports in use as well as the name of the container using them.
3 . Setup Nginx.
There is no configuration for Nginx, yet, so a basic config file must be created as /opt/docker/nginx/config/nginx.conf to look like this, the most minimalist Nginx configuration:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
} The ‘_’ means “any hostname” and the root location is relevant to the container. The docker-compose.yml file has mapped this location to /opt/docker/nginx/html on the local host.
Create a simple page at /opt/docker/nginx/html/index.html such as this:
<!DOCTYPE html>
<html>
<body>
<h1>My First Page</h1>
<p>My first words.</p>
</body>
</html>Now restart the nginx container (since it has a nice name, the name can be used instead of the container ID):
docker restart nginxAnd the basic web page should be visible at the local host IP address, since the docker-compose.yml file has mapped port 80 on the host to port 80 on the container.
If you have trouble, there should also be some logging happening in /opt/docker/nginx/logs/:
root@server40:/opt/docker# cat /opt/docker/nginx/logs/access.log
172.16.30.166 - - [25/Sep/2023:20:07:31 +0000] "GET / HTTP/1.1" 200 92 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" "-"
172.16.30.166 - - [25/Sep/2023:20:07:32 +0000] "GET /favicon.ico HTTP/1.1" 404 555 "http://172.16.30.40/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" "-"
The most common problem is file rights. With Nginx, the user must be owner of the entire path, from the root of the filesystem.
4 . Add another web service; Heimdall.
Heimdall is a neat tool to keep track of bookmarks and web based projects. It does not require Nginx to run, but it is an easy container to install and run.
Looking at the official site and Docker hub page, a basic configuration file might look like this (notice port 8080 is going to be used. Change this if it is already in use on your system):
heimdall:
image: lscr.io/linuxserver/heimdall:amd64-2.5.4
container_name: heimdall
environment:
- PUID=1000
- PGID=1000
- TZ=Europe\Berlin
volumes:
- "/opt/docker/heimdall/config/:/config/"
ports:
- 8080:80
restart: unless-stopped
This configuration can be appended to the previous to make a list of services and configurations that Docker must support. Again, beware of the indentation:
version: "3.3"
services:
nginx:
container_name: nginx
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- "/opt/docker/nginx/config:/etc/nginx/conf.d:ro"
- "/opt/docker/nginx/logs:/var/log/nginx"
- "/opt/docker/nginx/html:/usr/share/nginx/html"
- "/mnt/shares/certificates:/certificates"
restart: unless-stopped
heimdall:
image: lscr.io/linuxserver/heimdall:amd64-2.5.4
container_name: heimdall
environment:
- PUID=1000
- PGID=1000
- TZ=Europe\Berlin
volumes:
- "/opt/docker/heimdall/config/:/config/"
ports:
- "8080:80"
restart: unless-stopped
So you can see, docker-compose.yml is a readable format of a complicated Docker configuration. You can just keep appending build details as you grow. After a while, though, configuration files will be difficult to maintain and update. For repetitive or sensitive values, an environment file can be created.
5 . Docker environment files.
As the docker-compose script increases is complexity, there will be many places where a variable can provide a more consistent and convenient experience. There are many ways to do this, the method described here, is quite popular.
Create a file in the same folder as docker-compose.yml, called .env, and it has the following format VAR=string, one per line:
TIMEZONE="Europe/Berlin"
PASSWORD="Password123!"
CLOUDFLARE_DNS1="1.1.1.2"
CLOUDFLARE_DNS2="1.0.0.2"
ROUTER_DNS="172.16.20.1"
PUID="1000"
PGID="1000"
Each variable is placed into a docker-compose.yml file as such ${VAR} with a dollar and curly braces. This simple method means you can make changes across the infrastructure with just one setting:
version: "3.3"
services:
nginx:
container_name: nginx
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- "/opt/docker/nginx/config:/etc/nginx/conf.d:ro"
- "/opt/docker/nginx/logs:/var/log/nginx"
- "/opt/docker/nginx/html:/usr/share/nginx/html"
- "/mnt/shares/certificates:/certificates"
restart: unless-stopped
heimdall:
image: lscr.io/linuxserver/heimdall:amd64-2.5.4
container_name: heimdall
environment:
- "PUID=${PUID}"
- "PGID=${PGID}"
- "TZ=${TIMEZONE}"
volumes:
- "/opt/docker/heimdall/config/:/config/"
ports:
- "8080:80"
restart: unless-stopped
Additionally, root launches the docker containers and can read this environment variable file. Its only once the container is running, does each container take the PUID and GUIDs ascribed. This means the .env file can be locked down to root access only, and sensitive variables, such as initial passwords or API strings can be kept safer.
In the above example, Nginx runs as root, Heimdall runs as the PUIG and GUID values.
6 . Updating removing and maintaining Docker and Docker Images
Docker images can be pulled with “docker-compose pull” or “docker pull <image_name>” without interfering with the running container:
root@server40:/opt/docker# docker-compose pull
Pulling nginx ... done
Pulling heimdall ... done
Containers with updates need to be restarted, docker-compose will only restart those with new differences in configuration. Often the containerisation technology means there is no or little loss in service:
root@server40:/opt/docker# docker-compose up -d
heimdall is up-to-date
nginx is up-to-date
To individually update a container, the desired image must be pulled:
root@server40:/opt/docker# docker pull nginx:1.24.0
1.24.0: Pulling from library/nginx
7dbc1adf280e: Pull complete
a7184f3665ed: Pull complete
f144d5d97503: Pull complete
9097eea98b48: Pull complete
356d4b647b64: Pull complete
608e661a622a: Pull complete
Digest: sha256:73341830a31bf12a44c846b6b323dd8a4fab7668e72c16e9124913ff097c9536
Status: Downloaded newer image for nginx:1.24.0
docker.io/library/nginx:1.24.0
The container must be stopped:
root@server40:/opt/docker# docker stop nginx
nginx
The old container deleted:
root@server40:/opt/docker# docker rm nginx
nginxA new container made from the new image:
root@server40:/opt/docker# docker-compose up -d
heimdall is up-to-date
Creating nginx … doneNote, all data and configurations that want to be kept must be mapped to paths outside the container.
To remove a container permanently, stop it, then remove the container and remove its code-block from the docker-compose.yml file.
Note the data, files and configurations that are mapped out, still exist and need to be manually deleted.
7 . Supporting this blog.
This type of content takes a lot of effort to write. Each post is drafted, researched, then tested multiple timhis type of content takes a lot of effort to write. Each post is drafted, researched, then tested multiple times, even a simple step or detail might take more than a few hours to go from the idea to a published blog post.
If you feel I have saved you some time, you can support me by;
- hosting with DigitalOcean, like I do – DigitalOcean.
- buying me a beer through PayPal – PayPal.
© HorseFreeGlue, 2025. Unauthorized use and/or duplication of this material without express and written permission from this site’s author and/or owner is strictly prohibited