Why Gitea Actions containers disappeared after a successful job
The deployment job finished green, and its log showed both application containers starting. After the job ended, neither container was running on the VPS.
Gitea Runner's job-scoped Docker proxy was cleaning up the resources created during the task. Mounting the VPS Docker socket into the job bypassed that proxy. After the change, the user confirmed that the containers remained running.
The deployment looked successful
Its workflow checked out the repository, built the web and API images, and ran docker compose up -d --force-recreate. Compose logged a successful build and messages like:
Successfully tagged <image-name>:0.0.0
Container <container-name> Started
Compose's successful response confirms that Docker accepted the create and start requests. Check container state after runner cleanup, then test application health separately.
What the runner was doing
The VPS ran Gitea and a separate Gitea Runner service. The runner itself had /var/run/docker.sock mounted, so it could create the job container. The workflow ran inside that job container and used Docker CLI and Compose to deploy the application.
Mounting the socket into the runner gives the runner Docker access. It does not mean the job container talks to the host daemon directly. Gitea Runner can route job Docker commands through a proxy, which scopes resources to the task and removes them during cleanup. That works for temporary CI containers; a deployment needs its containers to outlive the task.
The host's docker info reported:
name=ldw-solutions root=/var/lib/docker
That identifies the VPS daemon. We did not have a matching report from inside the workflow, so the output alone could not show which daemon the job used. The containers disappearing at task cleanup fit Gitea Runner's documented proxy behavior.
Bypass the job-scoped Docker proxy
We changed the runner configuration to mount the host socket directly into each job container. docker_host: "-" disables the runner's automatic socket mount; container.options supplies the actual host socket mount, and valid_volumes allows that mount source:
container:
network: ldwsolutions-gitea-ci
privileged: false
docker_host: "-"
options: "--volume /var/run/docker.sock:/var/run/docker.sock"
valid_volumes:
- /var/run/docker.sock
The runner service also needs the host socket mounted into the runner container:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
We recreated the runner to load the change. The job workflow now fails if either containers is not running at the end of the job. After applying the configuration and rerunning the workflow, both containers remained running.
Security trade-off
A direct Docker socket mount gives a job broad control over the VPS Docker daemon. privileged: false does not reduce the authority of the mounted socket. Treat access as root-equivalent: restrict the runner to trusted repositories and never run untrusted pull-request workflows on it. A job can also inspect containers managed by the daemon, including the runner, so keep long-lived secrets out of the runner environment. For less-trusted workloads, use a separate worker host or a restricted SSH deployment account.
Takeaways
- A Compose
Startedmessage is not proof that a deployment survived runner cleanup. - The socket mounted into the runner and the socket available inside a job are different concerns.
- Job-scoped Docker proxies may clean up resources created during a workflow. For persistent deployment containers, configure an explicit host-socket mount and validate the runner's cleanup behavior.
- Verify the target daemon and check container state after deployment; add application health checks separately when readiness matters.