Platform

Docker-Based Local Development and Database Container Operations

A practical review of Docker images, containers, volumes, networks, Docker Hub, and local PostgreSQL/MySQL development environments.

Docker-Based Local Development and Database Container Operations

When you first learn Docker, you start with a focus on commands, such as docker run hello-world. However, where Docker becomes truly useful in a backend development environment is slightly different. When team members can launch dependent services such as application servers, PostgreSQL, MySQL, and Redis in the same way and test them on the same port and with the same initial data, Docker becomes not just a simple execution tool but a reference point for the development environment.

This article is a record of Docker basic commands, images and containers, Docker Hub, and MySQL/PostgreSQL container practices reorganized into the local backend development environment operation flow. The goal is not to memorize a lot of commands, but to check whether data is maintained when the container is restarted, where to look for logs, how to avoid port conflicts, and what assumptions are broken when moving to a production environment.

Separately understand images and containers

An image is a template for an execution environment, and a container is a unit that executes the image as an actual process. Images can contain application code, runtime, libraries, basic commands, and default environment variable values. A container is a running process created based on the image.

Image names usually have the following structure:

[Host[:Port]/[Namespace/]Repository[:Tag]]

ubuntu:24.04
postgres:16
ghcr.io/example/api:1.0.0
myregistry.local:5000/platform/api:2025.09.01

If you omit the tag, latest is usually added, but considering distribution and reproducibility, it is better not to rely on latest. Even with the same latest, the actual image content may change over time. It is convenient for local practice, but in CI/CD or Kubernetes deployment, using a clear version tag or digest is easier to track.

docker image pull ubuntu:20.04
docker image pull ubuntu:22.04
docker image pull ubuntu:24.04
docker image ls ubuntu

docker image pull ruby:3.2.2
docker image inspect ruby:3.2.2
docker image history ruby:3.2.2

The container shares the fate of the PID 1 process running inside it. Although a container may look like a “server,” it essentially runs a single main process in isolation. When the PID 1 process ends, the container terminates.

docker run --interactive --tty ubuntu bash
hostname
whoami
head -n 4 /etc/os-release
echo $SHELL
exit

docker container ls
docker container ls --all

Container states include created, running, paused, restarting, exited, dead, and removing. When dealing with backend services, looking at exited alone is not enough. You should also look at docker logs, exit code, and application initialization log to determine why it terminated.

Containers are also different from VMs. VMs run a guest OS on top of a hypervisor, but containers isolate processes while sharing the host kernel. So, it starts up quickly and the image is small, but due to the nature of sharing the kernel, you need to pay attention to security boundaries and permission settings.

Virtual Machine
  - hypervisor
  - guest OS per VM
  - strong isolation
  - heavier startup and image size

Container
  - shared host kernel
  - isolated process namespace
  - faster startup
  - needs careful image and runtime security

It is recommended not to run the operating image as root user. Not all images operate as non-root right away, but for application images, it is safer to clearly specify an execution account.

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN addgroup -S app && adduser -S app -G app
USER app
CMD ["node", "server.js"]

These settings are also connected to Kubernetes' securityContext. If you naturally use root privileges in your local Docker, the same habit can continue in your cluster.

Learn the flow of execution, connection, and deletion

The most basic implementation is as follows:

docker container run hello-world
docker container run --name hello hello-world
docker container ls --all
docker container rm hello

Interactive containers use the -it option.

docker container run --rm --interactive --tty python python3
>>> print("Hello Docker")
>>> exit()

Use exec when entering a container that is already running or executing a command.

docker container exec psdb head -n 4 /etc/os-release
docker container exec -it psdb bash

When cleaning up a container, you need to check whether it is running or stopped.

docker container stop <container_id_or_name>
docker container rm <container_id_or_name>
docker container rm --force <container_id_or_name>
docker container prune
`

```--rm` automatically deletes the container when it terminates. This is convenient for one-time testing, but if you need to check the status after termination, you may lose logs or container state. You need to be more careful with containers that have state, like databases.

## Port mapping should separate local development and production exposure

The port inside the container and the port accessed from the host are different. The NGINX container uses port 80 internally, but it can be mapped to 8080 locally.

```bash
docker container run --rm --publish 8080:80 nginx
curl http://localhost:8080

This mapping is in the order hostport:containerport. When running multiple services locally, port conflicts often occur. For example, if PostgreSQL is already using 5432 locally, you can map 5432 in the container to 15432 on the host.

docker container run \
  --name psdb \
  --detach \
  --env POSTGRES_PASSWORD=toor \
  --publish 15432:5432 \
  postgres:16

In the operating environment, the method of directly exposing ports to the outside changes. In Kubernetes, Service and Ingress are at the forefront, and in the cloud, Load Balancer or API Gateway is the entry point. Local -p 8080:80 is a convenience device for access verification, not an operational exposure design itself.

Container networks understand name-based communication first.

When running just one container locally, looking at localhost and port mapping seems sufficient. However, when API, DB, and Redis appear together, you need to understand communication between containers. In a Docker bridge network, containers attached to the same network can find each other by container name.

docker network create app-net

docker run --name postgres \
  --network app-net \
  -e POSTGRES_PASSWORD=app \
  -d postgres:16

docker run --name redis \
  --network app-net \
  -d redis:7

Within this network, the API container can be accessed through postgres:5432 and redis:6379. Port mapping is required when accessing from the host, but containers communicate with each other based on name within the same bridge network.

Host laptop
  -> localhost:15432
     -> postgres container:5432

API container
  -> postgres:5432
  -> redis:6379

If you try to connect to the DB as localhost:5432 from within the container without knowing this difference, it will fail. The localhost in the container is not the host but itself. This feeling continues in Kubernetes as well. The localhost, Service DNS, and Ingress host inside the Pod each have different boundaries.

Things to check when creating your own image

The simplest Dockerfile to put static HTML into an NGINX image could be written like this:

FROM nginx:1.27
COPY . /usr/share/nginx/html
docker build -t nginx:test .
docker run --rm -p 8080:80 nginx:test

Node.js applications usually define a working directory, install dependencies, and run commands.

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

A common mistake here is to unnecessarily add node_modules, build artifacts, and local configuration files to the image. .dockerignore must be managed together to make the image smaller and the build cache more stable.

node_modules
.next
dist
.env
.git

After building the image, check the history and size.

docker image ls
docker image history nginx:test
docker image inspect nginx:test

An operational image should be able to answer the following questions:

  • Is it possible to track from which source version it was built using tags?
  • Are there any build tools that are not needed at runtime left in the image?
  • Is the Secret or .env file included in the image layer?
  • Are the logs output to stdout/stderr rather than just stored in a file?
  • Does the application properly handle container termination signals?

Docker Hub and Registry are part of the deployment path

Docker Hub, GitHub Container Registry, AWS ECR, and Azure ACR are registries that store and distribute images. If you want your team or deployment pipeline to use a locally created image, you need to tag it and push it.

docker login
docker tag myapp:1.0 username/myapp:1.0
docker push username/myapp:1.0
docker pull username/myapp:1.0

In practice, you can connect to the httpd container, change index.html, and then create an image with docker commit.

docker pull httpd
docker run --name apache -p 8080:80 httpd
docker exec -it apache /bin/bash

docker commit apache username/apache-demo
docker push username/apache-demo

This method is good for understanding container and image relationships, but Dockerfile-based builds are better for actual collaboration. docker commit makes it difficult to review the code to see what changes have been made, and it is difficult to reproduce the same image. Images should be reproducible with Dockerfiles and CI logs whenever possible.

Database containers are one-time use without volumes

Running MySQL or PostgreSQL as a container makes local development easier. However, DB is a stateful system. If you delete the container, the internal file system will also disappear, so if you need to retain data, you must use a volume.

MySQL can create the initial root password, user, and DB as environment variables.

docker container run \
  --name mysql-db \
  --detach \
  --env MYSQL_ROOT_PASSWORD=toor \
  --env MYSQL_USER=tony \
  --env MYSQL_PASSWORD=pass1234 \
  --env MYSQL_DATABASE=mydb00 \
  --publish 3306:3306 \
  --volume mysql-data:/var/lib/mysql \
  mysql:8

After connecting, you can create a simple table.

mysql --host=127.0.0.1 --port=3306 --user=tony --password=pass1234 mydb00
show databases;
select database();
create table t1(id int, name varchar(10));
insert into t1 values (10, 'Jane');
insert into t1 values (20, 'Alice');
insert into t1 values (30, 'Tom');
select * from t1;

PostgreSQL can be run similarly.

docker container run \
  --name psdb \
  --detach \
  --env POSTGRES_PASSWORD=toor \
  --publish 5432:5432 \
  --volume postgres-data:/var/lib/postgresql/data \
  postgres:16
psql --host=127.0.0.1 --port=5432 --user=postgres

If the initial execution fails, check the log first.

docker container logs psdb
docker container logs --follow psdb

If you treat it as if you were operating a DB container, volumes, initialization scripts, account permissions, ports, and backups appear natural. In local development, the password can be kept simple for convenience, but in a real environment, secret management and network access restrictions must be considered together.

Volumes separate container lifetime and data. However, just because a volume exists does not mean it has been backed up. A volume is a device that retains data even if a container is deleted within the same host, and a backup involves creating a separate copy that can be recovered after a failure or mistake.

docker volume ls
docker volume inspect postgres-data

To dump a local PostgreSQL volume, you can use the tool inside the container.

docker exec psdb pg_dump -U postgres postgres > backup.sql

Recovery must also be checked separately.

cat backup.sql | docker exec -i psdb psql -U postgres postgres

Since the redirection processing method may be different in PowerShell, in actual operation automation, it is better to have a script verified based on CI runner or Linux shell. The important thing is not “the volume exists, so it is safe,” but whether you have ever created a backup file and restored it in another environment.

Launch dependent services together with Compose

In the actual back-end development environment, only an API server does not appear. PostgreSQL, Redis, message broker, search engine, and observability tools may be needed together. At this time, it is easier to manage by declaring dependent services with Docker Compose rather than repeating the docker run command for a long time.

services:
  api:
    build:
      context: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://app:app@postgres:5432/app
      REDIS_URL: redis://redis:6379
    depends_on:
      - postgres
      - redis

  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:7
    ports:
      - "6379:6379"

volumes:
  postgres-data:
docker compose up -d
docker compose ps
docker compose logs -f api
docker compose down
`

```depends_on` somewhat adjusts the container startup order, but does not guarantee that the database is actually ready to receive queries. The application must have a DB connection retry or readiness check. Even if you move to Kubernetes, the same problem repeats. The fact that a pod has appeared and the fact that the service is ready are different.

## Operational assumptions to check in your local Docker environment

While practicing Docker, the biggest criterion that remained wasIs it a reproducible execution unit?” rather thanThe container appears.” If you check the following items, you will go beyond simple practice and get closer to an actual development environment.

```txt
Runtime
- Does the container run as a foreground process?
- Does it shut down gracefully on SIGTERM?
- Are logs written to stdout/stderr?

Configuration
- Are environment-specific settings kept out of the image?
- Are secrets kept out of the Dockerfile and image layers?
- Are default ports and local port mappings documented?

Data
- Is database data stored in a volume?
- Can the initial schema or seed data be reproduced?
- Is the difference between deleting a container and deleting a volume understood?

Build
- Can the image be reproduced from the Dockerfile?
- Is `.dockerignore` cleaned up?
- Are image tags connected to deployment history?

This standard continues in Kubernetes as well. If the Docker image is unstable, the deployment is also unstable. If environment variables are mixed locally, Helm values ​​are also mixed. Cluster log collection becomes difficult if logs are not output to standard output. Therefore, it is correct to view Docker as a starting point for creating an operational application unit, rather than a light exercise in the pre-Kubernetes stage.

Cleanup

Docker is a tool for running containers, but in back-end development, it is a reference point for consistently reproducing the local environment. By distinguishing between images and containers and understanding PID 1, logs, port mapping, volume, registry, Dockerfile, and Compose together, the problem of “It only works on my computer” can be significantly reduced.

In particular, if you treat dependent services such as PostgreSQL, MySQL, and Redis as containers, you will get a sense of how to isolate and maintain a stateful system. If you delete a container, you will see for yourself what is lost, what is kept when you leave a volume, and what initialization will not occur again if you change an environment variable.

This flow continues to Kubernetes. If Docker does not clearly handle images, ports, environment variables, volumes, and logs, Kubernetes' Deployment, Service, ConfigMap, Secret, and PVC also become unclear. Conversely, if you make your local Docker environment reproducible, you'll have much less wobble when moving on to CI and cluster deployment.

Unauthorized copying, redistribution, and automated scraping are not permitted. Please cite the original URL when referencing this article.