Run One-Off App Tasks, Migrations, and Admin Commands Safely with Docker Compose

Use Docker Compose with stronger preflight checks, safer command patterns, and better cleanup so maintenance tasks do not turn into accidental production changes.

Docker ComposeOne-off operationsMigrations
What you learn

How to choose between docker compose exec and docker compose run, inspect the resolved stack, run admin commands with less guesswork, and verify the result before you move on.

Best for

Schema migrations, user-management commands, maintenance scripts, imports, exports, and other tasks that should be deliberate instead of improvised.

Risk to watch

The most common failure is running the right command in the wrong context: wrong service, wrong environment file, wrong project, or a fresh container when you meant the live one.

Before you begin

  • Know which Compose files and env files define the target environment.
  • Know the service name that should run the command, not just the image you think it uses.
  • Know whether the task should run against the live container state or a fresh one-off container.
  • Have a backup or rollback boundary ready if the command can change data.

Many self-hosting mistakes happen because operators treat every administrative task like a quick shell trick. Docker Compose is better than that when you use it deliberately. It can show the resolved configuration, run a command inside an already-running service, or create a short-lived one-off container that inherits the service configuration. Those are different tools for different jobs.

Expected outcome: You will run app tasks with clearer environment targeting, safer cleanup, and less chance of accidentally acting on the wrong container or the wrong stack.

Step 1: Choose docker compose exec or docker compose run on purpose

docker compose exec runs a command inside an already-running container. That makes it the better fit for live debugging, environment inspection, and commands that should use the exact container instance already serving traffic.

docker compose exec app env | rg 'DATABASE_URL|REDIS|SMTP'
docker compose exec app python manage.py shell

docker compose run creates a new one-off container from the service definition. Docker's official Compose reference calls out two important differences: the command overrides the service command, and published service ports are not created unless you ask for them.

docker compose run --rm app python manage.py createsuperuser
docker compose run --rm app python manage.py migrate

Use run when you want a clean short-lived task container. Use exec when you need the live service context.

Warning: If you use run casually, Compose may start declared dependencies for you and you may assume you are interacting with the already-running app when you are not. If you do not want dependency startup, add --no-deps.

Step 2: Inspect the real Compose configuration first

Before touching production or staging, inspect what Compose will actually apply. This matters even more if you use multiple files or environment-specific overrides.

docker compose config

This is the fastest way to catch the wrong image tag, the wrong env file path, missing variable substitution, or a service definition that is not the one you thought you were about to use.

If your stack uses optional tools or staging-only services, inspect the profile-aware configuration too:

docker compose --profile tools config

Compose profiles are useful for optional admin or debug services because explicitly targeted profile services can be run without bringing unrelated profile members up at the same time.

Step 3: Preflight the live stack

Do not run the admin command first. Confirm the stack you are about to touch:

docker compose ps
docker compose logs --tail=50 app
docker compose logs --tail=50 worker

This gives you three things before the change:

  • proof that the expected services are running
  • a recent baseline of logs before the command lands
  • a quick way to see whether the app is already degraded before you create a second problem

If the task will change data, also confirm maintenance mode, worker pause state, or backup freshness before you continue.

Step 4: Run one-off tasks safely

For migrations, imports, or repair commands that belong in a fresh task container, start with docker compose run --rm. The --rm flag removes the task container when it exits, which is better than leaving mystery containers behind.

# Example database migration
docker compose run --rm app python manage.py migrate

# Example app-specific repair or import task
docker compose run --rm app ./bin/import-data --source /tmp/import.csv

If the task should not automatically start dependencies, use --no-deps and make dependency readiness your own deliberate check:

docker compose run --rm --no-deps app ./bin/report-active-users

If you are redirecting output to a host file, use non-interactive execution so TTY behavior does not garble the result:

docker compose exec -T db pg_dump -U app appdb > appdb-prechange.sql

That small -T habit matters for clean dumps and exports.

Step 5: Run commands inside a running container safely

Use exec when the task should happen inside the currently running service instance, such as checking environment variables, testing service discovery, or opening a framework shell against the live app container:

docker compose exec app env | rg 'DATABASE_URL|REDIS|SMTP'
docker compose exec app sh

This is especially useful for debugging because it tells you what the live service can see right now, not what a fresh one-off task container would see after a slightly different startup path.

Be careful with write actions in exec. If the live container is serving traffic, you are changing the environment that is already carrying user work.

Step 6: Treat migrations as a controlled change, not a casual command

Migrations deserve more discipline than a normal admin command. Before you run them:

  • confirm the current image or release version
  • confirm the backup point
  • pause or drain background workers if they can write during the schema change
  • decide whether the migration belongs in a one-off container or in the app container during a maintenance window

A safe sequence often looks like this:

docker compose config
docker compose ps
docker compose logs --tail=50 app
docker compose run --rm app python manage.py migrate
docker compose logs --tail=100 app

If the application has a separate migration service in a Compose profile, explicitly targeting that service is often cleaner than ad hoc shelling into the main app container.

Step 7: Verify the outcome and clean up

Success is not just an exit code. After the task completes, confirm what actually changed:

  • the expected service is still healthy
  • logs do not show a follow-on error
  • the admin command did what you expected
  • temporary task containers did not accumulate
docker compose ps
docker compose logs --tail=100 app
docker compose logs --tail=100 worker

If the task was a migration, test one real app path before reopening workers or traffic. If it was a data export or report task, confirm the output file is complete and readable.

Expected outcomes

  • You know when to use exec and when to use run.
  • You inspect the resolved Compose config before risky tasks.
  • One-off task containers are removed cleanly with --rm.
  • Migrations and admin commands happen with explicit verification instead of blind trust.

Troubleshooting

The command ran against the wrong environment: check docker compose config, the working directory, and any -f or --env-file flags you used.

The task container behaves differently from the live app: you probably needed exec instead of run, or the one-off container started with a different dependency state.

A migration succeeded, but the app fails after: inspect logs immediately and verify worker state, app version, and environment variables before retrying the migration.

Compose started more services than expected: run can start dependencies. Use --no-deps when that is not what you want.

The exported file is empty or malformed: use exec -T for redirected command output and confirm you are writing the file on the host side, not inside the container.

Warning: The most expensive Compose mistakes are not syntax mistakes. They are context mistakes. Slow down long enough to confirm the project, service, environment, and rollback point before you run anything that writes.

What to do next

Once one-off operations are predictable, the next maturity step is freezing background work before risky changes. Continue with How to Put a Self-Hosted App Into Maintenance Mode for Safe Updates and Migrations.