> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy ClamAV on Upsun, on service mode.

> Learn how to deploy ClamAV service on Upsun. This tutorial covers ClamAV integration on service scan mode.  
  


export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud",
      "linkedin": "https://www.linkedin.com/in/laurent-arnoud-861b44121/"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["theosakamg", "flovntp"], date: "2025-04-24T12:04:20+01:00", image: "https://www.clamav.net/assets/clamav-trademark.png" }} />

In a [previous article](/posts/tutorials/deploying-clamav-onetime), we integrated ClamAV in **one-time scan mode**. This mode takes time since it scans the integrity of a file tree.\
In this new article, we will integrate ClamAV in **Service mode** in order to scan files on demand. But let's start with a reminder about ClamAV

## What is ClamAV?

ClamAV (Clam AntiVirus) is an open-source **antivirus engine** designed for detecting malware, viruses, and other malicious threats. It is widely used for scanning file servers, and web applications. ClamAV is known for its lightweight nature and ability to be integrated into various security systems.

![Logo ClamAV](https://www.clamav.net/assets/clamav-trademark.png)

In this tutorial we will be implementing ClamAV in **service mode**.

## Context

In this tutorial, we will simulate a typical application requirement.\
Our example application, built in PHP (though this approach can be applied to different languages and frameworks), allows users to upload files.
Before integrating the uploaded files into the filesystem, we want them to be scanned by ClamAV for security purposes.

Workflow:

```mermaid theme={null}
graph LR;
    Upsun-->PHP_upload;
    PHP_upload-->ClamAV_scan;
    ClamAV_scan-->FS;
```

This tutorial builds on the setup from the [previous article](/posts/tutorials/deploying-clamav-onetime).

At this stage, we already have:

* ClamAV binaries installed via [Composable Image](https://docs.upsun.com/create-apps/app-reference/composable-image.html)
* Writable mount points for uploaded files (`data`)
* Necessary mounts for ClamAV to function properly (`var`)

To integrate ClamAV efficiently, we will run it in daemon mode within the `clamav` container.

For this tutorial, we will introduce a new PHP application to upload files and use the existing `clamav` application to check uploaded files.
So we will need to:

* transform our project in a [multiple application structure](https://docs.upsun.com/create-apps/multi-app.html)
* run ClamAV in [Daemon mode](https://docs.clamav.net/manual/Usage/Scanning.html?highlight=Socket#daemon)
* add a new `app` PHP application to upload files

## Prepare your ClamAV project

The initial step will be to prepare our source code architecture to welcome a frontend application that will display a PHP upload form.

### Create a preview environment

As we never work on the production environment, we will create a dedicated [preview environment](https://docs.upsun.com/glossary.html#preview-environment).
To create a preview environment, use the [following command line](https://docs.upsun.com/administration/cli/reference.html#environmentbranch).
It will create, on the fly, an exact copy of your parent environment (here, branch `main`, so it's your production environment).

```bash {filename="Terminal"} theme={null}
upsun environment:branch php-upload-form
```

This will create and switch to a new local Git branch `php-upload-form` and deploy the corresponding preview environment in less than a few minutes.

## Transform project with a multiple application structure

To prepare the project for handling multiple applications, move all existing ClamAV source code into a dedicated `clamav` folder.

From the root of your project source code, execute the following command lines:

```bash {filename="Terminal"} theme={null}
mkdir -p clamav
for file in * .[^.]*; do
  if [[ "$file" != "clamav" && "$file" != ".upsun" && "$file" != ".git" ]]; then
    mv -- "$file" clamav/
  fi
done
```

Then, in your `.upsun/config.yaml` file, update the [`source.root` parameter](https://docs.upsun.com/create-apps/app-reference/single-runtime-image.html#root-directory) of your `app` application:

```yaml {filename=".upsun/config.yaml",linenos=table,hl_lines=["4"],linenostart=1,base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
applications:
  clamav:
    source:
      root: "/clamav"
    #...
```

## Setting up ClamAV

### Enable ClamAV On-Access scanning mode

In order to run ClamAV as a service ([On-Access scanning mode](https://docs.clamav.net/manual/OnAccess.html)), we first need to enable the TCP listening mode in its configuration.
To do so, update your `clamav/etc/clamd.conf` file with these 2 lines at the end:

```text {filename="clamav/etc/clamd.conf",linenos=table,hl_lines=["9-10"],linenostart=1,base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
# Log section
LogFile /app/var/log/clamav.log
LogFileMaxSize 5M
LogTime yes

# Configure path
DatabaseDirectory /app/var/lib

#TCPAddr localhost
TCPSocket 3310
```

Then, update the `clamav/scripts/clam_install.sh` file with the following:

```bash {filename="clamav/scripts/clam_install.sh",linenos=table,hl_lines=["10-11","15-16"],linenostart=1,base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
#!/usr/bin/env bash
# -*- coding: utf-8 -*-

echo "Prepare folder for clamav..."
mkdir -p \
    "${PLATFORM_APP_DIR}/var/log" \
    "${PLATFORM_APP_DIR}/var/lib" \
    "${PLATFORM_APP_DIR}/var/etc" \
    "${PLATFORM_APP_DIR}/data/folder2scan" \
    "${PLATFORM_APP_DIR}/data/quarantine" \
    "${PLATFORM_APP_DIR}/data/uploads" 

echo "Generate client/server config..."
cp "${PLATFORM_APP_DIR}/etc/clamd.conf" "${PLATFORM_APP_DIR}/var/etc/"
IP=$(ifconfig eth0 | grep "inet " | awk -F'[: ]+' '{ print \$3 }')
sed -i "s/#TCPAddr localhost/TCPAddr ${IP}/g" "${PLATFORM_APP_DIR}/var/etc/clamd.conf"
```

This will get the current container IP and set it to the corresponding `TCPAddr` ClamAV variable.

### Add ClamAV to SystemD

To start ClamAV as a service, we can execute `clamd` binary within our `clamav` container:

```bash {filename="Terminal"} theme={null}
clamd --config-file="/app/var/etc/clamd.conf"
```

However, in case of an error, there is no guarantee that the service will remain available.
To ensure continuous operation, we need to implement a watchdog mechanism.
Upsun provides [SystemD](https://en.wikipedia.org/wiki/Systemd) in userspace mode for this purpose.

Let's start by defining a service unit file `clamav/scripts/systemd.d/clamav.service`:

```SYSTEMD {filename="clamav/scripts/systemd.d/clamav.service",base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
[Unit]
Description=Clamav Service
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=1
TimeoutSec=30s
ExecStart=/app/.local/bin/clamd --config-file="/app/var/etc/clamd.conf" --foreground=true
ExecReload=/bin/kill -USR2 $MAINPID
ExecStartPre=/bin/mkdir -p /var/run/clamav
```

To make this unit loadable by SystemD from userspace, we need to structure the unit files directory properly.

```bash {filename="clamav/scripts/systemd_install.sh",base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
#!/usr/bin/env bash
# -*- coding: utf-8 -*-

echo "Create Systemd user folder structure..."
mkdir -p ~/.config/systemd/user

echo "Copy Systemd user unit services..."
cp -R scripts/systemd.d/* ~/.config/systemd/user/
```

Then, add the unit to SystemD.

```bash {filename="clamav/scripts/systemd_init.sh",base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
#!/usr/bin/env bash
# -*- coding: utf-8 -*-

echo "Reload Services"
systemctl --user daemon-reload

echo "Activating with user"
systemctl --user enable clamav.service

echo "Starting with user"
systemctl --user start clamav.service

echo "Show current services"
systemctl --user status clamav.service
```

And let's add execution permissions to the script.

```bash {filename="Terminal"} theme={null}
chmod +x clamav/scripts/*
```

And,integrate all these elements into Upsun to ensure they are applied with each deployment,
by calling `./scripts/systemd_install.sh` in your `hooks.build`, and `./scripts/systemd_init.sh` in your `hooks.deploy`:

```yaml {filename=".upsun/config.yaml",linenos=table,hl_lines=["14-16","20"],linenostart=1,base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
applications:
  clamav:
    source:
      root: "/clamav"
    stack:
      - "clamav"
    build:
      flavor: none
    container_profile: BALANCED
    mounts:
      "data": { source: storage, source_path: data } # Mount of data to scan.
      "var": { source: storage, source_path: var } # Mount of configuration/database for ClamAV.
    hooks:
      build: |
        set -eu
        ./scripts/systemd_install.sh
      deploy: |
        set -eu
        ./scripts/clam_install.sh
        ./scripts/systemd_init.sh
    #...
```

### Deploying ClamAV on Upsun

We need to push these additions to the Upsun project.

```bash {filename="Terminal"} theme={null}
git add .
git commit -m "Add ClamAV service to SystemD"
upsun push
```

After deployment, you can check that ClamAV is listening to port `3310` by connecting to the `clamav` container and execute `netstat -laputne` command line:

```bash {filename="Terminal",linenos=table,hl_lines=["5"],linenostart=1} theme={null}
upsun ssh --app clamav -- netstat -laputne
#...
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State  User  Inode      PID/Program name
tcp        0      0 0.0.0.0:3310  0.0.0.0:*       LISTEN 10000 425818084  308/clamd
```

<Note>
  **Troubleshoot**: If you don't, please be sure that the ClamAV database has been initialized, at least once.
  If not, execute the following command line:

  ```bash {filename="Terminal"} theme={null}
  upsun ssh --app=clamav -- ./scripts/clam_update-db.sh
  ```
</Note>

## Add `app` PHP application

This basic PHP application will display a basic form to upload a file and then use an external library ([`appwrite/php-clamav`](https://github.com/appwrite/php-clamav)) to perform the ClamAV scan.

### Create an `app` folder

Our `app` source code will be located in a `app` folder. To create this folder, execute the following:

```bash {filename="Terminal"} theme={null}
mkdir -p app
```

### Create the upload form

All the logic of this PHP form will be located in a `public/index.php` file, with the following:

```php {filename="app/public/index.php",base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
<h2>Upload and scan a file</h2>
<?php
require_once $_ENV['PLATFORM_APP_DIR'] . '/vendor/autoload.php';

use Appwrite\ClamAV\Network;

if (isset($_POST['upload_form'])) {
  $clam = new Network($_ENV['CLAMAV_HOST'], '3310');
  
  if ($clam->ping()) { // Check ClamAV is up and running
    $scanDestination = $_ENV['PLATFORM_APP_DIR'] . "/data/folder2scan/" . $_FILES['upfile']['name'];
    $uploadDestination = $_ENV['PLATFORM_APP_DIR'] . "/data/uploads/" . $_FILES['upfile']['name'];
    $quarantineDestination = $_ENV['PLATFORM_APP_DIR'] . "/data/quarantine/" . $_FILES['upfile']['name'];
    
    // Move candidate file to a shared folder with clamav app
    move_uploaded_file($_FILES['upfile']['tmp_name'], $scanDestination);
    
    if ($clam->fileScan($scanDestination)) {
      rename($scanDestination, $uploadDestination);
      echo "</br>No virus detected in " . $_FILES['upfile']['name'];
    } else {
      rename($scanDestination, $quarantineDestination); // move infected file to the data/quarantine folder
      echo "</br>You try to upload an infected file or the file does not exist, you should use an antivirus on your local machine.";
    }
  } else {
    echo "</br>ClamAV is not running, starting ClamAV daemon can take some time, please retry later";
  }
}
?>

<form method="POST" action="<?php echo $_SERVER['PHP_SELF']; ?>" enctype="multipart/form-data">
  <label for="file">File to Upload:</label></br>
  <input type="file" name="upfile" required></br>
  <input type="submit" value="Upload" name="upload_form"/>
</form>
```

Please note that before scanning this candidate file, we first move the file into the shared `/data/folder2scan/` folder for `clamav` app being able to scan the file.
If ClamAV does not detect any threat, then the file is moved to a final `/data/uploads/` folder, moved otherwise in the `/data/quarantine/` folder.

### Import `appwrite/php-clamav` library

We will need to import [`appwrite/php-clamav`](https://github.com/appwrite/php-clamav) library using Composer.

From the `app` folder, execute the following command line:

```bash {filename="Terminal"} theme={null}
cd app
composer require appwrite/php-clamav
cd ..
```

### Configure `app` application

Then, we need to declare a new `app` Upsun application.

Update your `.upsun/config.yaml` file with the following:

```yaml {filename=".upsun/config.yaml",linenos=table,hl_lines=["4-29"],linenostart=1,base_url="https://github.com/upsun/demo-clamav/tree/php-upload-form/"} theme={null}
applications:
  clamav:
    #...
  app:
    source:
      root: "/app"
    type: php:8.3
    container_profile: BALANCED
    web:
      locations:
        "/":
          root: "public"
          expires: 1h
          passthru: "/index.php"
    mounts:
      "var": { source: storage, source_path: var, service: clamav }
      "data": { source: storage, source_path: data, service: clamav }
    hooks:
      build: |
        set -x -e
        composer install
    relationships:
      clamav:
        service: "clamav"
        endpoint: "http"

routes:
  "https://{all}/": { type: upstream, upstream: "app:http", primary: true}
  "http://{all}/": { type: redirect, to: "https://{all}/" }
```

<Note>
  Some information on this `app` configuration:

  * **line 16-17**: please note the `service: clamav` parameter. This additional parameter allows `app` and `clamav` apps sharing the same NFS folders (see [Share a mount between several apps documentation page](https://docs.upsun.com/create-apps/app-reference/single-runtime-image.html#share-a-mount-between-several-apps)). This will be useful for our `app` application to upload files to scan in a directory shared with the `clamav` application (ClamAV scan only local files).
  * **line 25-28**: we add a [relationship](https://docs.upsun.com/create-apps/multi-app/relationships.html#relationships-example) from `app` to `clamav`. This will enable internal communication between apps and expose environment variables within the `app` container with `clamav` app info (see `CLAMAV_*` envVars in your `app` container).
</Note>

## Deploy

Finally, we need to push all of this on our Upsun project.

```bash {filename="Terminal"} theme={null}
git add .
git commit -m "Add app application"
upsun push
```

## Test it

You can then open the frontend by using the following command line:

```bash {filename="Terminal"} theme={null}
upsun environment:url --primary
```

You should end up on this frontend page:

<img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/deploying-clamav-service/frontend-homepage.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=e0f5f13decc8a577a85d3011f657bd71" alt="" width="546" height="296" data-path="images/posts/tutorials/deploying-clamav-service/frontend-homepage.webp" />

You can start playing with safe and infected files.

As mention in the [previous episode](/posts/tutorials/deploying-clamav-onetime),
you can simulate an infected file by creating a local `fake.txt` file with such a source code:

```text {filename="fake.txt"} theme={null}
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
```

This is a [typical `Eicar-Signature`](https://en.wikipedia.org/wiki/EICAR_test_file) virus,
and is totally safe for your local machine and for your Upsun application.

<Note>
  **Please note**: some local antivirus software, as [SentinelOne](https://fr.sentinelone.com/),
  will detect this file as a virus and will immediately move them, after saving it, to your local quarantine folder,
  and so, this **infected** file will disappear from your machine.
  We had a hard time finding a way to test it.

  **Tips**: we used a non-protected laptop (😇) to upload it.
</Note>

After uploading this `fake.txt` file, you should get such a response:

<img src="https://mintcdn.com/upsun-c9761871/OR9CPqO13fyi9LoK/images/posts/tutorials/deploying-clamav-service/frontend-infection-detected.webp?fit=max&auto=format&n=OR9CPqO13fyi9LoK&q=85&s=265d9f040f8457d503cdb1fd4a6a2397" alt="" width="1742" height="352" data-path="images/posts/tutorials/deploying-clamav-service/frontend-infection-detected.webp" />

## Conclusion

Both methods (one-time scan and service mode) can coexist within the same project.\
For example, we can scan files at the time of upload to ensure immediate security checks while also running a full scan of the entire file repository every night.

***

Project on our [Github Upsun](https://github.com/upsun/demo-clamav/tree/php-upload-form/) (branch `php-upload-form`)
