├── .gitignore
├── .github
├── CODEOWNERS
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── dependabot-auto.yml
│ └── publish.yml
├── test.sh
├── .editorconfig
├── _config.yml
├── Dockerfile
├── src
├── docker-entrypoint.sh
└── vsftpd.conf
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @garethflowers
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: "https://paypal.me/garethflowers"
2 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -xe
3 | export IMAGE_NAME=garethflowers/ftp-server
4 |
5 | docker build --tag $IMAGE_NAME .
6 | docker run --rm $IMAGE_NAME sh -c 'vsftpd -version 0>&1'
7 |
8 | echo "\nOK"
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = tab
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.yml]
12 | indent_size = 2
13 | indent_style = space
14 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | docker_repository: garethflowers/ftp-server
2 | author: garethflowers
3 | plugins:
4 | - jekyll-remote-theme
5 | - jekyll-sitemap
6 | remote_theme: garethflowers/garethflowers.github.io
7 | tagline: Gareth Flowers
8 | twitter:
9 | username: garethflowers
10 | card: summary
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | - package-ecosystem: "docker"
8 | commit-message:
9 | include: "scope"
10 | prefix: "chore"
11 | directory: "/"
12 | schedule:
13 | interval: "daily"
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.22.1
2 | ENV FTP_USER=foo \
3 | FTP_PASS=bar \
4 | GID=1000 \
5 | UID=1000 \
6 | PUBLIC_IP=0.0.0.0
7 |
8 | RUN apk add --no-cache --update \
9 | vsftpd==3.0.5-r2
10 |
11 | COPY [ "/src/vsftpd.conf", "/etc" ]
12 | COPY [ "/src/docker-entrypoint.sh", "/" ]
13 |
14 | ENTRYPOINT [ "/docker-entrypoint.sh" ]
15 | CMD [ "/usr/sbin/vsftpd" ]
16 | EXPOSE 20/tcp 21/tcp 40000-40009/tcp
17 | HEALTHCHECK CMD netstat -lnt | grep :21 || exit 1
18 |
--------------------------------------------------------------------------------
/src/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | FTP_DATA=/home/$FTP_USER
3 | FTP_GROUP=$( getent group "$GID" | awk -F ':' '{print $1}' )
4 |
5 | if test -z "$FTP_GROUP"; then
6 | FTP_GROUP=$FTP_USER
7 | addgroup -g "$GID" -S "$FTP_GROUP"
8 | fi
9 |
10 | adduser -D -G "$FTP_GROUP" -h "$FTP_DATA" -s "/bin/false" -u "$UID" "$FTP_USER"
11 |
12 | mkdir -p $FTP_DATA
13 | chown -R "$FTP_USER:$FTP_GROUP" "$FTP_DATA"
14 | echo "$FTP_USER:$FTP_PASS" | /usr/sbin/chpasswd
15 |
16 | sed -i -r "s/0.0.0.0/$PUBLIC_IP/g" /etc/vsftpd.conf
17 |
18 | touch /var/log/vsftpd.log
19 | tail -f /var/log/vsftpd.log | tee /dev/stdout &
20 | touch /var/log/xferlog
21 | tail -f /var/log/xferlog | tee /dev/stdout &
22 |
23 | if [ "$*" = "/usr/sbin/vsftpd" ]; then
24 | /usr/sbin/vsftpd
25 | else
26 | exec "$@"
27 | fi
28 |
--------------------------------------------------------------------------------
/src/vsftpd.conf:
--------------------------------------------------------------------------------
1 | # daemon
2 | background=NO
3 | listen_ipv6=NO
4 | listen=YES
5 | session_support=NO
6 |
7 | # access
8 | anonymous_enable=NO
9 | ftpd_banner=FTP Server
10 | local_enable=YES
11 |
12 | # local user
13 | allow_writeable_chroot=YES
14 | chroot_local_user=YES
15 | guest_enable=NO
16 | local_umask=022
17 | passwd_chroot_enable=YES
18 |
19 | # local time
20 | use_localtime=YES
21 |
22 | # directory
23 | dirlist_enable=YES
24 | dirmessage_enable=NO
25 | hide_ids=YES
26 |
27 | # file transfer
28 | write_enable=YES
29 |
30 | # logging
31 | dual_log_enable=YES
32 | log_ftp_protocol=YES
33 | xferlog_enable=YES
34 |
35 | # network
36 | connect_from_port_20=NO
37 | ftp_data_port=20
38 | max_clients=0
39 | max_per_ip=0
40 | pasv_address=0.0.0.0
41 | pasv_addr_resolve=YES
42 | pasv_promiscuous=YES
43 | pasv_enable=YES
44 | pasv_max_port=40009
45 | pasv_min_port=40000
46 | port_enable=YES
47 |
48 | # tweaks
49 | seccomp_sandbox=NO
50 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-auto.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot PR Approve and Merge
2 | on: pull_request_target
3 | permissions:
4 | contents: write
5 | pull-requests: write
6 | jobs:
7 | dependabot:
8 | runs-on: ubuntu-latest
9 | if: ${{ github.actor == 'dependabot[bot]' }}
10 | steps:
11 | - name: Dependabot Metadata
12 | id: dependabot-metadata
13 | uses: dependabot/fetch-metadata@v2.4.0
14 | with:
15 | github-token: "${{ secrets.GITHUB_TOKEN }}"
16 | - name: Approve a PR
17 | run: gh pr review --approve "$PR_URL"
18 | env:
19 | PR_URL: ${{ github.event.pull_request.html_url }}
20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 | - name: Enable auto-merge for Dependabot PRs
22 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}
23 | run: gh pr merge --auto --squash "$PR_URL"
24 | env:
25 | PR_URL: ${{ github.event.pull_request.html_url }}
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Gareth Flowers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Build and Publish Images
2 | on:
3 | pull_request:
4 | branches:
5 | - main
6 | push:
7 | branches:
8 | - main
9 | tags:
10 | - "*.*.*"
11 | jobs:
12 | publish:
13 | name: Publish
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 | - name: Prepare
19 | id: prep
20 | uses: docker/metadata-action@v5.9.0
21 | with:
22 | images: |
23 | ${{ github.repository_owner }}/ftp-server
24 | ghcr.io/${{ github.repository_owner }}/ftp-server
25 | tags: |
26 | type=edge,branch=main
27 | type=semver,pattern={{version}}
28 | type=semver,pattern={{major}}.{{minor}}
29 | type=semver,pattern={{major}}
30 | - name: Set up QEMU
31 | uses: docker/setup-qemu-action@v3
32 | - name: Set up Docker Buildx
33 | uses: docker/setup-buildx-action@v3
34 | - name: Login to DockerHub
35 | if: github.event_name != 'pull_request'
36 | uses: docker/login-action@v3
37 | with:
38 | username: ${{ secrets.DOCKERHUB_USERNAME }}
39 | password: ${{ secrets.DOCKERHUB_TOKEN }}
40 | - name: Login to GHCR
41 | if: github.event_name != 'pull_request'
42 | uses: docker/login-action@v3
43 | with:
44 | registry: ghcr.io
45 | username: ${{ github.repository_owner }}
46 | password: ${{ secrets.GITHUB_TOKEN }}
47 | - name: Build and Push
48 | uses: docker/build-push-action@v6
49 | with:
50 | context: .
51 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
52 | push: ${{ github.event_name != 'pull_request' }}
53 | tags: ${{ steps.prep.outputs.tags }}
54 | labels: ${{ steps.prep.outputs.labels }}
55 | - name: Update Description
56 | continue-on-error: true
57 | uses: peter-evans/dockerhub-description@v4
58 | with:
59 | password: ${{ secrets.DOCKERHUB_TOKEN }}
60 | repository: ${{ github.repository_owner }}/ftp-server
61 | short-description: ${{ github.event.repository.description }}
62 | username: ${{ secrets.DOCKERHUB_USERNAME }}
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FTP Server
2 |
3 | A simple FTP server, using
4 | [`vsftpd`](https://security.appspot.com/vsftpd.html).
5 |
6 | ## How to use this image
7 |
8 | ### Start a FTP Server instance
9 |
10 | To start a container, with data stored in `/data` on the host, use the
11 | following:
12 |
13 | #### ... via `docker run`
14 |
15 | ```sh
16 | docker run \
17 | --detach \
18 | --env FTP_PASS=123 \
19 | --env FTP_USER=user \
20 | --env PUBLIC_IP=192.168.0.1 \
21 | --name my-ftp-server \
22 | --publish 20-21:20-21/tcp \
23 | --publish 40000-40009:40000-40009/tcp \
24 | --volume /data:/home/user \
25 | garethflowers/ftp-server
26 | ```
27 |
28 | #### ... via `docker compose`
29 |
30 | ```yml
31 | services:
32 | ftp-server:
33 | container_name: my-ftp-server
34 | environment:
35 | - PUBLIC_IP=192.168.0.1
36 | - FTP_PASS=123
37 | - FTP_USER=user
38 | image: garethflowers/ftp-server
39 | ports:
40 | - "20-21:20-21/tcp"
41 | - "40000-40009:40000-40009/tcp" # For passive mode
42 | volumes:
43 | - "/data:/home/user"
44 | ```
45 |
46 | ## Configuration
47 |
48 | ### Ports
49 |
50 | | Port | Required? | Description | Config |
51 | | -------------- | --------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
52 | | 21 | Required | The default port that FTP listens on. | [listen_port](https://security.appspot.com/vsftpd/vsftpd_conf.html) |
53 | | 20 | Required | The default port for PORT connections to originate. | [ftp_data_port](https://security.appspot.com/vsftpd/vsftpd_conf.html) |
54 | | 40000
40009 | Optional | The min and max ports to use for PASV connections to originate. | [pasv_min_port](https://security.appspot.com/vsftpd/vsftpd_conf.html)
[pasv_max_port](https://security.appspot.com/vsftpd/vsftpd_conf.html) |
55 |
56 | ### Environment Variables
57 |
58 | | Variable | Default Value | Description |
59 | | ----------- | ------------- | ------------------------------------------------- |
60 | | `FTP_PASS` | `bar` | The FTP password |
61 | | `FTP_USER` | `foo` | The FTP username |
62 | | `UID` | `1000` | User ID for the `$FTP_USER` user |
63 | | `GID` | `1000` | Group ID for the `$FTP_USER` user |
64 | | `PUBLIC_IP` | `0.0.0.0` | Public IP address to use for Passive connections. |
65 |
66 | ## License
67 |
68 | - This image is released under the
69 | [MIT License](https://raw.githubusercontent.com/garethflowers/docker-ftp-server/master/LICENSE).
70 |
--------------------------------------------------------------------------------